import { CCClass } from "cc";
import { Constructor } from "..";
import { Logger } from "../../common/log/Logger";
import { BaseProvider } from "../provider/BaseProvider";
import ClassProvider from "../provider/ClassProvider";
import SingletonProvider from "../provider/SingletonProvider";
import ValueProvider from "../provider/ValueProvider";
import { getClass, INJECT_CLASS_NAME_TAG, INJECT_INFO_LIST_TAG, setClass } from "../typed/injector-typed";


export type InstanceParamType<T extends { init(...param: any[]): void }> = T extends { init(...args: infer P): void } ? P : never;

export class Injector {

    public static default: Injector = null;

    private readonly _providerMap: Map<string, BaseProvider> = new Map();

    public parentInjector: Injector = null;

    /**
     * 设置provider
     * @param key 
     * @param provider 
     * @returns 
     */
    public setProvider(key: string, provider: BaseProvider): this {
        if (!this._providerMap.has(key)) {
            this._providerMap.set(key, provider)
        }

        return this;
    }

    /**
     * 获取provider options
     * @param key 
     * @returns 
     */
    public getProvider(key: string): BaseProvider {
        if (this._providerMap.has(key)) {
            return this._providerMap.get(key);
        } else {
            return undefined;
        }
    }

    /**
     * 通过映射key 获取实例
     * @param key 
     * @returns 
     */
    public getInstance<T>(identifier: string | Constructor<T>, ...args: any[]): T | undefined {
        let key: string;
        if (typeof identifier == "string") {
            key = identifier
        } else {
            let ctor: Constructor = identifier;
            key = ctor.prototype[INJECT_CLASS_NAME_TAG];
        }

        const provider = this.getProvider(key);
        if (provider) {
            if (provider instanceof ClassProvider) {
                return provider.getEntity(...args) as T;
            } else if (provider instanceof SingletonProvider) {
                return provider.getEntity(...args) as T;
            } else if (provider instanceof ValueProvider) {
                return provider.getEntity(...args) as T;
            } else {
                return provider.getEntity(...args) as T;
            }

        } else if (this.parentInjector) {
            return this.parentInjector.getInstance(key, ...args) as T;
        } else {
            return undefined;
        }
    }

    getValue(identifier: string) {
        let provider = this.getProvider(identifier);
        if (provider) {
            if (provider instanceof ValueProvider)
                return provider.getEntity();
            else
                throw new Error('getValue: not fond privider');
        } else if (this.parentInjector) {
            return this.parentInjector.getValue(identifier);
        } else {
            return undefined;
        }
    }

    getCustomRule(identifier: string, ...args: any[]): any | undefined {
        let provider = this.getProvider(identifier);
        if (provider) {
            return provider.getEntity(...args);
        } else if (this.parentInjector) {
            return this.parentInjector.getCustomRule(identifier, ...args);
        } else {
            return undefined;
        }
    }

    public instantiateOf(clazzOrName: Constructor | string, ...args: any[]) {
        let entity = null;
        let clazz: Constructor = null;
        if (typeof clazzOrName === 'string') {
            clazz = getClass(clazzOrName);
        } else {
            clazz = clazzOrName;
        }

        entity = new clazz(args);
        Injector.injectObject(entity, this);

        return entity;
    }

    public static injectObject(entity, injector?: Injector): void {
        entity.injector = injector ? injector : Injector.default;
        let inject_list = Reflect.get(entity, INJECT_INFO_LIST_TAG);
        if (inject_list != null) {

            for (let option of inject_list) {
                entity[option.propertyName] = option.value(option.injector);
                // entity[option.propertyName]?.onInject();
            }
        }
    }

    /**
     * 映射class provider
     * @param key 
     * @param clazz 
     * @returns 
     */
    public mapClass(key: string, clazz: Constructor): Injector {
        setClass(key, clazz);

        if (!this._providerMap.has(key)) {
            let provider = new ClassProvider(key);
            this.mapCustomRule(key, provider);
        } else {
            throw `duplicate class, name: ${key}`;
        }
        return this;
    }

    public mapSingleton(key: string, clazz: Constructor): Injector {
        setClass(key, clazz);

        if (!this._providerMap.has(key)) {
            let provider = new SingletonProvider(key);
            this.mapCustomRule(key, provider);
        } else {
            Logger.error('A Class already mapping! its name: [', key, ']');
        }
        return this;
    }

    /**
     * 映射value provider
     * @param key 
     * @param value 
     * @returns 
     */
    public mapValue(key: string, value: any): Injector {
        if (!this._providerMap.has(key)) {
            let provider = new ValueProvider(value);
            this.mapCustomRule(key, provider);
        }
        return this;
    }

    public mapCustomRule(key: string, provider: any): Injector {
        if (!this._providerMap.has(key)) {
            provider.setInjector(this);
            this.setProvider(key, provider);
        }

        return this;
    }

    public unmap(key: any): Injector {
        if (this._providerMap.has(key)) {
            this._providerMap.delete(key);
        } else if (this.parentInjector) {
            this.parentInjector.unmap(key);
        }
        return this;
    }

    public unmapAll(): Injector {
        this._providerMap.clear();
        return this;
    }

    public hasMapping(key: any): boolean {
        if (this._providerMap.has(key)) {
            return true;
        } else if (this.parentInjector) {
            return this.parentInjector.hasMapping(key);
        }

        return false;
    }

    /**
     * fork一个新的注入器，并设置当前为父节点
     * @returns 新的Injector
     */
    public fork(): Injector {
        const childInjector = new Injector();
        childInjector.parentInjector = this;
        return childInjector;
    }

}

/**
 *  跟注入器
 */

Injector.default = new Injector();


export const injectProperty = function (clazz: Constructor, propertyName, propertyType) {
    CCClass.Attr.setClassAttr(clazz, propertyName, 'inject', propertyType);
}

